package mathtools;

import exceptions.MatrixSizeException;
import mathtools.Complex;
import mathtools.Matrix;

/**
 * Representation of a matrix of elements. Class includes support
 * for matrix of NxM elements, and elements are Complex elements, 
 * allowing for real and complex number operations.
 * @author Alex Bush, Richard Inskip, James Hancox
 */
public class Matrix {

	/** 2D array representation of a matrix */
	protected Complex[][] m;
	/** Width of the matrix */
	protected int width;
	/** Height of matrix */
	protected int height;

	/**
	 * Create an empty matrix of integers of size (NxM) 
	 * @param N width of matrix
	 * @param M height of matrix
	 * @param num number for all elements in matrix
	 */
	public Matrix(int N,int M, double num){
		if (N < 1 || M < 1){
			throw new MatrixSizeException("Dimensions of matrix must be at least 1x1");
		}
		m = new Complex[N][M];
		for ( int i = 0; i < N; i++ ){
			for ( int j = 0; j < M; j++ ){
				m[i][j] = new Complex(num,0);
			}
		}
		width = N;
		height = M;
	}

	/**
	 * Create an empty matrix with no assigned elements.
	 * Better performance for operations where all elements are guaranteed
	 * to be set (e.g. Tensor product and Multiplication)
	 * @param N width of matrix
	 * @param M height of matrix 
	 */
	private Matrix(int N,int M){
		if (N < 1 || M < 1){
			throw new MatrixSizeException("Dimensions of matrix must be at least 1x1");
		}
		m = new Complex[N][M];
		width = N;
		height = M;
	}

	/**
	 * Return the width of this matrix
	 * @return Width of the current matrix
	 */
	public int getWidth(){
		return m.length;
	}

	/**
	 * Return the height of this matrix
	 * @return Height of the current matrix
	 */
	public int getHeight(){
		return m[0].length;
	}

	/**
	 * Compute the tensor product of two matrices.
	 * @param b Matrix to tensor with
	 * @return c the tensor product this matrix with b
	 */
	public Matrix tensorProduct(Matrix b){
		Matrix c = new Matrix(getWidth()*b.getWidth(),getHeight()*b.getHeight());

		for ( int i = 0; i < getWidth(); i++ ){
			for ( int j = 0; j < getHeight();j++ ){
				for ( int k = 0; k < b.getWidth(); k++ ){
					for ( int l = 0; l < b.getHeight(); l++){
						c.setElement(i*b.getWidth()+k,j*b.getHeight()+l,Complex.multiplyComplex(m[i][j],b.getElement(k,l)));
					}
				}
			}
		}
		return c;
	}

	/**
	 * Return single element of a matrix.
	 * @param i ith column
	 * @param j jth row
	 * @return Complex element
	 */
	public Complex getElement(int i, int j){
		if (i >= this.getWidth()){
			throw new IllegalArgumentException("Argument reference element out of array bounds in matrix width:" + i);
		}
		else if (j >= this.getHeight()){
			throw new IllegalArgumentException("Argument reference element out of array bounds in matrix height:" + j);
		}
		return m[i][j];
	}

	/**
	 * Set an element of this matrix.
	 * @param i ith column
	 * @param j jth row
	 * @param e Complex element to set
	 */
	public void setElement(int i, int j, Complex e){
		if (i >= this.getWidth()){
			throw new IllegalArgumentException("Argument reference element out of array bounds in matrix width:" + i);
		}
		else if (j >= this.getHeight()){
			throw new IllegalArgumentException("Argument reference element out of array bounds in matrix height:" + j);
		}
		m[i][j] = e;
	}

	/**
	 * Multiply this matrix by another matrix. Width of this must equal height of b
	 * @param b matrix to multiply this by
	 * @throws illegal argument exception
	 */
	public Matrix multiplyMatrix(Matrix b){
		if ( this.getWidth() != b.getHeight()){
			throw new MatrixSizeException("Width of this matrix must be equal to the height of the passed matrix b:\n" +
					"this width: " + this.getWidth() + " b height: " + b.getHeight());
		}
		Matrix c = new Matrix(b.getWidth(),this.getHeight());

		for ( int i=0; i < c.getHeight(); i++){
			for ( int j = 0; j < c.getWidth(); j++){
				Complex e = new Complex(0,0);
				for ( int k = 0; k < this.getWidth(); k++){
					e = Complex.addComplex(e, Complex.multiplyComplex(this.getElement(k, i), b.getElement(j, k)));
				}
				c.setElement(j, i, e);
			}
		}
		return c;
	}

	/**
	 * A method to multiply a matrix by a constant.
	 * @param constant Constant represents any double to multiply the matrix by
	 * @return Matrix c
	 */
	public Matrix multiplyMatrix(double constant){
		Matrix c = new 	Matrix(this.getWidth(),this.getHeight());

		for ( int i=0; i < c.getHeight(); i++){
			for ( int j = 0; j < c.getWidth(); j++){
				c.setElement(j, i,Complex.multiplyComplex(constant,this.getElement(j, i)));
			}
		}
		return c;
	}

	/**
	 * A method to multiply a matrix by a constant complex number.
	 * @param constant A constant Complex number to multiply a matrix
	 * @return Matrix c
	 */
	public Matrix multiplyMatrix(Complex constant){
		Matrix c = new 	Matrix(this.getWidth(),this.getHeight());

		for ( int i=0; i < c.getHeight(); i++){
			for ( int j = 0; j < c.getWidth(); j++){
				c.setElement(j, i,Complex.multiplyComplex(constant,this.getElement(j, i)));
			}
		}
		return c;
	}

	/**
	 *  Method to multiply a matrix by itself power number of times.
	 * @param pow The Number of times to multiply the matrix by itself.
	 * @return Matrix c Answer.
	 */
	public Matrix powerMatrix(int pow){
		if(pow<1){
			throw new IllegalArgumentException("ERROR: Power Must be greater than 0.");
		}

		Matrix c = new Matrix(this.getWidth(),this.getHeight());

		c=this;
		for ( int i=1; i<pow;i++){
			c = c.multiplyMatrix(this);
		}
		return c;
	}

	/**
	 * Add a matrix to this matrix. Matrices must have same dimension
	 * @param b Matrix to add to this
	 * @return c Matrix this + b
	 * @throws Illegal argument exception
	 */
	public Matrix addMatrix(Matrix b){
		// Deal with exception
		if ( this.getWidth()!= b.getWidth() || this.getHeight() != b.getHeight()){
			throw new MatrixSizeException("Dimensions of the two matrices must be the same.\n" +
					"this: " + this.getWidth() + "x" + this.getHeight() + "\n" +
					"b: " + b.getWidth() + "x" + b.getHeight());
		}
		// Simple matrix addition
		Matrix c = new Matrix(this.getWidth(),this.getHeight());
		for ( int i = 0; i < this.getWidth(); i++){
			for ( int j = 0; j < this.getHeight(); j++){
				c.setElement(i, j,Complex.addComplex(this.getElement(i, j), b.getElement(i, j)));
			}
		}
		return c;
	}

	/**
	 * Method returns the transpose of this matrix
	 * @return Matrix transpose of this
	 */
	public Matrix transposeMatrix(){
		Matrix m = new Matrix(this.getHeight(),this.getWidth());
		for (int i = 0; i < this.getHeight(); i++){
			for ( int j = 0; j < this.getWidth(); j++){
				m.setElement(i, j, this.getElement(j, i));
			}
		}
		return m;
	}

	/**
	 * Return a matrix as a string for printing to the terminal.
	 * Output will span as many lines as there are rows
	 * @return String of a matrix
	 */
	public String toString(){
		String out = "";
		for (int i = 0; i < this.getHeight(); i++){
			out+="|";
			for (int j = 0; j < this.getWidth(); j++){
				out+= " "+this.getElement(j, i).toString();
			}
			out+=" |\n";
		}
		return out;
	}

	/**
	 * 
	 * Create an identity matrix. 
	 * Matrix will be size x size square matrix.
	 * @param size Size of the matrix
	 * @return Identity matrix
	 */
	public static Matrix IdentityMatrix(int size){
		Matrix identity = new Matrix(size,size,0);
		for (int i=0; i<size; i++){
			identity.setElement(i, i, new Complex(1,0));
		}
		return identity;
	}
}
